package org.lora.core.mybatis;

import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

/**
 * <p>
 * Title:PaginationInterceptor
 * </p>
 * <p>
 * Desription:分页插件
 * </p>
 * Create Time:2016年7月7日 下午1:22:11
 * 
 * @author Bladnir@outlook.com
 *         <p>
 *         History:
 *         </p>
 *         2016年7月7日 Bladnir Create</br>
 */
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class,
		ResultHandler.class }) })
public class PaginationInterceptor implements Interceptor {

	private static final int MAPPED_STATEMENT_INDEX = 0;
	private static final int PARAMETER_INDEX = 1;
	private static final int ROWBOUNDS_INDEX = 2;

	private Dialect dialect;

	/**
	 * 构造方法
	 * 
	 * @param dialect
	 */
	public PaginationInterceptor(Dialect dialect) {
		this.dialect = dialect;
	}

	@Override
	public Object intercept(Invocation inv) throws Throwable {

		final Object[] queryArgs = inv.getArgs();

		RowBounds rowBounds = (RowBounds) queryArgs[ROWBOUNDS_INDEX];

		// 查找方法参数中的 分页请求对象
		Page page = this.findPageableObject(queryArgs[PARAMETER_INDEX]);

		// 如果需要分页
		if (page != null) {
			final MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
			final Object parameter = queryArgs[PARAMETER_INDEX];

			final BoundSql boundSql = ms.getBoundSql(parameter);

			final StringBuilder bufferSql = new StringBuilder(boundSql.getSql().trim());
			if (bufferSql.lastIndexOf(";") == bufferSql.length() - 1) {
				bufferSql.deleteCharAt(bufferSql.length() - 1);
			}

			String originalSql = bufferSql.toString();

			if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
				rowBounds = new RowBounds(page.getPageSize() * (page.getPageNumber() - 1), page.getPageSize());
			}

			// 分页查询 本地化对象 修改数据库注意修改实现
			String pagesql = dialect.getLimitString(originalSql, rowBounds.getOffset(), rowBounds.getLimit());

			queryArgs[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);

			BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pagesql, boundSql.getParameterMappings(),
					boundSql.getParameterObject());

			for (ParameterMapping mapping : boundSql.getParameterMappings()) {
				String prop = mapping.getProperty();
				if (boundSql.hasAdditionalParameter(prop)) {
					newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
				}
			}
			MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));

			queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
		}

		return inv.proceed();
	}

	/**
	 * 在方法参数中查找 分页请求对象
	 * 
	 * @param params
	 *            Mapper接口方法中的参数对象
	 * @return
	 */
	private Page findPageableObject(Object params) {

		if (params == null) {
			return null;
		}

		if (Page.class.isAssignableFrom(params.getClass())) {
			return (Page) params;
		}

		else if (params instanceof ParamMap) {
			@SuppressWarnings("unchecked")
			ParamMap<Object> paramMap = (ParamMap<Object>) params;
			for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
				Object paramValue = entry.getValue();

				if (paramValue == null) {
					continue;
				}

				if (Page.class.isAssignableFrom(paramValue.getClass())) {
					return (Page) paramValue;
				}
			}
		}

		return null;
	}

	private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
		Builder builder = new Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
			StringBuilder keyProperties = new StringBuilder();
			for (String keyProperty : ms.getKeyProperties()) {
				keyProperties.append(keyProperty).append(",");
			}
			keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
			builder.keyProperty(keyProperties.toString());
		}

		// setStatementTimeout()
		builder.timeout(ms.getTimeout());

		// setStatementResultMap()
		builder.parameterMap(ms.getParameterMap());

		// setStatementResultMap()
		builder.resultMaps(ms.getResultMaps());
		builder.resultSetType(ms.getResultSetType());

		// setStatementCache()
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());
		builder.useCache(ms.isUseCache());

		return builder.build();
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		// just do nothing
	}

	/**
	 * <p>
	 * Title:BoundSqlSqlSource
	 * </p>
	 * <p>
	 * Desription:
	 * </p>
	 * Create Time:2016年7月7日 下午1:21:43
	 * 
	 * @author Bladnir@outlook.com
	 *         <p>
	 *         History:
	 *         </p>
	 *         2016年7月7日 Bladnir Create</br>
	 */
	public static class BoundSqlSqlSource implements SqlSource {
		BoundSql boundSql;

		/**
		 * 构造方法
		 * 
		 * @param boundSql
		 */
		public BoundSqlSqlSource(BoundSql boundSql) {
			this.boundSql = boundSql;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.apache.ibatis.mapping.SqlSource#getBoundSql(java.lang.Object)
		 */
		@Override
		public BoundSql getBoundSql(Object parameterObject) {
			return boundSql;
		}
	}
}
